package com.vr.xuecheng.media.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vr.xuecheng.base.exception.XueChengPlusException;
import com.vr.xuecheng.base.response.PageResult;
import com.vr.xuecheng.base.response.Result;
import com.vr.xuecheng.media.mapper.MediaFilesMapper;
import com.vr.xuecheng.media.model.dto.MediaFilesPageQueryParamsDTO;
import com.vr.xuecheng.media.model.dto.UploadFileDTO;
import com.vr.xuecheng.media.model.entity.MediaFiles;
import com.vr.xuecheng.media.model.vo.MediaFilesPageVO;
import com.vr.xuecheng.media.model.vo.UploadFileVO;
import com.vr.xuecheng.media.service.IMediaFilesService;
import com.vr.xuecheng.media.service.IMediaProcessService;
import com.vr.xuecheng.media.util.MinioUtil;
import io.minio.ComposeSource;
import io.minio.StatObjectResponse;
import io.minio.messages.DeleteObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Stream;

/**
 * <p>
 * 媒资信息 服务实现类
 * </p>
 *
 * @author hzh
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class MediaFilesServiceImpl extends ServiceImpl<MediaFilesMapper, MediaFiles> implements IMediaFilesService {
    private final MediaFilesMapper mediaFilesMapper;
    @Value("${minio.bucket-name.media-name}")
    private String mediafilesBucket;
    @Value("${minio.bucket-name.video-name}")
    private String videoBucket;
    private final MinioUtil minioUtil;
    private final IMediaProcessService mediaProcessService;
    @Autowired
    @Lazy
    private IMediaFilesService mediaFilesService;


    @Override
    public PageResult<MediaFilesPageVO> page(MediaFilesPageQueryParamsDTO req) {
        Page<MediaFiles> page = new Page<>(req.getPageNo(), req.getPageSize());
        Page<MediaFiles> mediaFilesPage = mediaFilesMapper.selectPage(page, new QueryWrapper<MediaFiles>()
                .lambda()
                .like(!StrUtil.isBlank(req.getFilename()), MediaFiles::getFilename, req.getFilename())
                .eq(!StrUtil.isBlank(req.getType()), MediaFiles::getFileType, req.getType())
                .eq(!StrUtil.isBlank(req.getAuditStatus()), MediaFiles::getAuditStatus, req.getAuditStatus())
                .orderByDesc(MediaFiles::getCreateDate)
                .orderByDesc(MediaFiles::getChangeDate)

        );
        List<MediaFilesPageVO> mediaFilesPageVOList = BeanUtil.copyToList(mediaFilesPage.getRecords(), MediaFilesPageVO.class);
        return new PageResult<>(mediaFilesPageVOList, mediaFilesPage.getTotal(), mediaFilesPage.getCurrent(), mediaFilesPage.getSize());
    }

    @Transactional
    @Override
    public int deleteById(String fileId) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileId);
        String objectName = mediaFiles.getFilePath();
        boolean isExist = minioUtil.checkFileExists(objectName, mediaFiles.getBucket());
        if (isExist) {
            minioUtil.deleteFile(objectName, mediaFiles.getBucket());
        }
        // minio里无论存不存在，都要删除文件信息
        return mediaFilesMapper.deleteById(fileId);
    }

    @Override
    public boolean checkFile(String fileMd5) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles != null) {
            // 还得检查minio里存不存在
            if (!minioUtil.checkFileExists(mediaFiles.getFilePath(), mediaFiles.getBucket())) {
                // 删除数据库中的文件信息，因为同一个文件的md5值可能相同
                mediaFilesMapper.deleteById(fileMd5);
                return false;
            } else {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean checkChunk(String fileMd5, int chunkIndex) {
        String objectName = this.getChunkFilePath(fileMd5) + chunkIndex;
        boolean isExist = minioUtil.checkFileExists(objectName, videoBucket);
        if (isExist) {
            log.debug("分块文件已存在：{}", videoBucket + "/" + objectName);
            return true;
        }
        return false;
    }

    @Override
    public Result<Boolean> uploadChunk(MultipartFile file, String fileMd5, int chunkIndex) {
        String objectName = this.getChunkFilePath(fileMd5) + chunkIndex;
        boolean isUpload = false;
        try {
            isUpload = minioUtil.uploadFile(file.getInputStream(), file.getSize(), objectName, videoBucket, file.getContentType());
        } catch (IOException e) {
            log.error("分块上传异常：{}", videoBucket + "/" + objectName);
        }
        if (!isUpload) {
            log.error("分块上传失败：{}", videoBucket + "/" + objectName);
            return Result.error(false, "分块上传失败");
        }
        log.debug("分块上传成功：{}", videoBucket + "/" + objectName);
        return Result.ok(true);
    }

    @Override
    @Transactional
    public Result<Boolean> mergeChunks(String fileMd5, String fileName, int chunkTotal) {
        // 通过minio 合并分块文件
        String objectName = getMergeFilePathByMd5(fileMd5, fileName.substring(fileName.lastIndexOf(".")));
        // 获取合并分块的源文件也就是分块文件
        List<ComposeSource> sources = Stream.iterate(0, i -> i + 1)
                .limit(chunkTotal)
                .map(i -> ComposeSource.builder()
                        .bucket(videoBucket)
                        .object(this.getChunkFilePath(fileMd5) + i).build()
                ).toList();
        boolean isMerge = minioUtil.mergeChunks(objectName, videoBucket, sources);
        if (!isMerge) {
            return Result.error(false, "合并文件失败");
        }
        // 校验文件完整性
        try (InputStream inputStream = minioUtil.downloadFile(objectName, videoBucket);) {
            if (inputStream == null) {
                log.error("下载合并后文件失败,objectName:{}", objectName);
                return Result.error(false, "下载合并后文件失败");
            }
            // 计算md5值
            String chunkFileMd5 = DigestUtils.md5Hex(inputStream);
            // 比对md5值
            if (!chunkFileMd5.equals(fileMd5)) {

                log.error("合并前MD5:{}，合并后MD5:{},所以文件不完整", fileMd5, chunkFileMd5);
                return Result.error(false, "文件不完整，md5值不匹配");
            }
        } catch (Exception e) {
            log.error("合并文件校验失败");
            return Result.error(false, "合并文件校验失败");
        }
        // 保存文件信息到数据库
        // 获取文件的元信息
        UploadFileDTO uploadFileDTO = UploadFileDTO.builder()
                // TODO 登录后完善
                .companyId(1232141425L)
                .companyName(null)
                .fileType("001002")
                .filename(fileName)
                .auditStatus("002002")
                .tags("课程视频")
                .build();
        StatObjectResponse statObjectResponse = minioUtil.getStat(objectName, videoBucket);
        if (statObjectResponse != null) {
            uploadFileDTO.setFileSize(statObjectResponse.size());
        }
        mediaFilesService.saveUploadFileToDb(fileMd5, uploadFileDTO, objectName, videoBucket);
        // 无论是否合并成功都要删除分块文件
        this.clearChunkFiles(fileMd5, videoBucket, chunkTotal);
        minioUtil.deleteFile(this.getChunkFilePath(fileMd5), videoBucket);
        return Result.ok("文件合并成功", true);
    }


    private void clearChunkFiles(String fileMd5, String videoBucket, int chunkTotal) {
        // 获取合并分块的源文件也就是分块文件
        List<DeleteObject> deleteObjectList = Stream.iterate(0, i -> i + 1)
                .limit(chunkTotal)
                .map(i -> new DeleteObject(this.getChunkFilePath(fileMd5) + i)
                ).toList();
        minioUtil.deleteFiles(videoBucket, deleteObjectList);
    }

    /**
     * @param fileMd5 md5值
     * @param exName  文件的扩展名
     * @return {@code String } 文件的路径
     * 通过md5值和文件名获取文件路径
     */
    private String getMergeFilePathByMd5(String fileMd5, String exName) {
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + fileMd5 + exName;
    }

    /**
     * @param fileMd5 md5值
     * @return {@code String } 文件的目录
     */
    private String getChunkFilePath(String fileMd5) {
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

    @Override
    @Transactional
    public UploadFileVO upload(MultipartFile file) {
        // 1、上传文件到minio
        String md5 = getMd5(file);
        // 检查数据库是否存在其文件信息
        MediaFiles mediaFiles = mediaFilesMapper.selectById(md5);
        // 组装objectName,如2025/03/11/1.mp4
        String objectName = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyyy/MM/dd") + "/" + md5 + "_" + file.getOriginalFilename();
        String contentType = file.getContentType();
        if (mediaFiles != null) {
            // 还得判断minion里文件是否存在，不存在的话，删除数据库原来的文件信息并重新上传文件并
            if (!minioUtil.checkFileExists(objectName, mediafilesBucket)) {
                // 删除数据库中的文件信息，因为同一个文件的md5值可能相同
                mediaFilesMapper.deleteById(md5);
            } else {
                return BeanUtil.toBean(mediaFiles, UploadFileVO.class);
            }
        }
        //上传文件
        boolean isUpload = false;
        try {
            isUpload = minioUtil.uploadFile(file.getInputStream(), file.getSize(), objectName, mediafilesBucket, contentType);
        } catch (IOException e) {
            log.error("上传图片：{},失败:{}", mediafilesBucket + "/" + objectName, e.getMessage());
        }
        if (!isUpload) {
            XueChengPlusException.cast("上传文件失败");
        }
        // 2、保存文件信息到数据库
        UploadFileDTO uploadFileDTO = UploadFileDTO.builder()
                // TODO 登录后完善
                .companyId(1232141425L)
                .companyName(null)
                .fileType("001001")
                .filename(file.getOriginalFilename())
                .tags("课程图片")
                .auditStatus("002002")
                .fileSize(file.getSize()).build();

        return mediaFilesService.saveUploadFileToDb(md5, uploadFileDTO, objectName, mediafilesBucket);
    }

    @Override
    public Boolean upload(MultipartFile file, String objectName) {
        InputStream inputStream = null;
        boolean isUpload = false;
        try {
            inputStream = file.getInputStream();
            isUpload = minioUtil.uploadFile(inputStream, file.getSize(), objectName, mediafilesBucket, file.getContentType());
        } catch (IOException e) {
            log.error("上传文件失败，文件：{}", mediafilesBucket + objectName);
        }
        return isUpload;
    }



    @Transactional
    @Override
    public UploadFileVO saveUploadFileToDb(String md5, UploadFileDTO uploadFileDTO, String objectName, String bucketName) {
        MediaFiles mediaFiles = BeanUtil.toBean(uploadFileDTO, MediaFiles.class)
                .setId(md5)
                .setBucket(bucketName)
                .setFilePath(objectName)
                .setFileId(md5)
                .setUrl("/" + bucketName + "/" + objectName)
                // TODO 登录后添加
                .setUsername(null)
                .setStatus("1")
                .setRemark(null)
                .setAuditMind(null);
        int result = mediaFilesMapper.insert(mediaFiles);
        if (result < 0) {
            log.error("保存文件信息失败：{}", mediaFiles.toString());
            XueChengPlusException.cast("保存文件信息失败");
        }
        // 添加到待处理任务列表
        mediaProcessService.addWaitingTask(mediaFiles);
        return BeanUtil.toBean(mediaFiles, UploadFileVO.class);
    }

    /**
     * @param file 文件对象
     * @return {@code String } md5值
     */
    private String getMd5(MultipartFile file) {
        try (InputStream inputStream = file.getInputStream()) {
            return DigestUtils.md5Hex(inputStream);
        } catch (IOException e) {
            e.getStackTrace();
            log.error("获取md5失败:{}", e.getMessage());
            return null;
        }
    }
}
